package com.hoho.android.usbserial.driver;

import android.hardware.usb.UsbConstants;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbEndpoint;
import android.hardware.usb.UsbInterface;
import android.hardware.usb.UsbRequest;
import android.util.Log;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.LinkedHashMap;
import java.util.Map;

public class CdcTusb3410SerialDriver extends CommonUsbSerialDriver {

    
    /**
     * USB CDC/ACM serial driver implementation.
     *
     * @author Christoph Rathfelder (chriostoph.rathfelder@hsg-imit.de)
     * 
     * Developed using the the {@link CdcAcmSerialDriver} as a basis to allow serial communication using TI's TUSB3410 chip
     * 
     * @see <a href="http://code.google.com/p/usb-serial-for-android/">USB Serial
     * for Android project page</a>
     */
    
    private static final int USB_RECIP_INTERFACE = 0x01;
    private static final int USB_RT_ACM = UsbConstants.USB_TYPE_CLASS | USB_RECIP_INTERFACE;

    private static final int SET_LINE_CODING = 0x20;  // USB CDC 1.1 section 6.2
    private static final int GET_LINE_CODING = 0x21;
    private static final int SET_CONTROL_LINE_STATE = 0x22;
    private static final int SEND_BREAK = 0x23;
    
    private boolean mRts = false;
    private boolean mDtr = false;    
    
    
    private final String TAG = CdcAcmSerialDriver.class.getSimpleName();
    
    private UsbInterface mInterface;    
    private UsbEndpoint mMControlEndpoint;
    
    private UsbEndpoint mReadEndpoint;
    private UsbEndpoint mWriteEndpoint;
    
  
  
    public CdcTusb3410SerialDriver(UsbDevice device, UsbDeviceConnection connection) {
        super(device, connection);
    }

    /**
     * @see com.hoho.android.usbserial.driver.CommonUsbSerialDriver#open()
     * In contrast to {@link AcmSerialDriver}, the TUSB3410 chip uses the second and third endpoint of the first USB interface as communication endpoints.
     * First endpoint is control, second one is outbound and last one is inbound.
     */
    @Override
    public void open() throws IOException {
        Log.d(TAG, "claiming interfaces, count=" + mDevice.getInterfaceCount());

        Log.d(TAG, "Claiming control interface.");
        mInterface = mDevice.getInterface(0);
        Log.d(TAG, "Control iface=" + mInterface);


        if (!mConnection.claimInterface(mInterface, true)) {
            throw new IOException("Could not claim control interface.");
        }
        mMControlEndpoint = mInterface.getEndpoint(0);
        Log.d(TAG, "Control endpoint direction: " + mMControlEndpoint.getDirection());

            
      
        mWriteEndpoint = mInterface.getEndpoint(1);
        Log.d(TAG, "Write endpoint direction: " + mWriteEndpoint.getDirection());
        mReadEndpoint = mInterface.getEndpoint(2);
        Log.d(TAG, "Read endpoint direction: " + mReadEndpoint.getDirection());

    }

    @Override
    public void close() throws IOException {
       mConnection.close();

    }

    @Override
    public int read(byte[] dest, int timeoutMillis) throws IOException {
        final int numBytesRead;
        synchronized (mReadBufferLock) {
            int readAmt = Math.min(dest.length, mReadBuffer.length);
            numBytesRead = mConnection.bulkTransfer(mReadEndpoint, mReadBuffer, readAmt,
                    timeoutMillis);
            if (numBytesRead < 0) {
                // This sucks: we get -1 on timeout, not 0 as preferred.
                // We *should* use UsbRequest, except it has a bug/api oversight
                // where there is no way to determine the number of bytes read
                // in response :\ -- http://b.android.com/28023
                // should be updated in Android 4.2.1
                return 0;
            }
            System.arraycopy(mReadBuffer, 0, dest, 0, numBytesRead);
        }
        return numBytesRead;
    }
    

   
    
    @Override
    public int write(byte[] src, int timeoutMillis) throws IOException {
        // TODO: Not tested since the current device is a source only
        int offset = 0;

        while (offset < src.length) {
            final int writeLength;
            final int amtWritten;

            synchronized (mWriteBufferLock) {
                final byte[] writeBuffer;

                writeLength = Math.min(src.length - offset, mWriteBuffer.length);
                if (offset == 0) {
                    writeBuffer = src;
                } else {
                    // bulkTransfer does not support offsets, make a copy.
                    System.arraycopy(src, offset, mWriteBuffer, 0, writeLength);
                    writeBuffer = mWriteBuffer;
                }

                amtWritten = mConnection.bulkTransfer(mWriteEndpoint, writeBuffer, writeLength,
                        timeoutMillis);
            }
            if (amtWritten <= 0) {
                throw new IOException("Error writing " + writeLength
                        + " bytes at offset " + offset + " length=" + src.length);
            }

            Log.d(TAG, "Wrote amt=" + amtWritten + " attempted=" + writeLength);
            offset += amtWritten;
        }
        return offset;
    }

    @Override
    public void setParameters(int baudRate, int dataBits, int stopBits, int parity)
            throws IOException {
        byte stopBitsByte;
        switch (stopBits) {
            case STOPBITS_1: stopBitsByte = 0; break;
            case STOPBITS_1_5: stopBitsByte = 1; break;
            case STOPBITS_2: stopBitsByte = 2; break;
            default: throw new IllegalArgumentException("Bad value for stopBits: " + stopBits);
        }

        byte parityBitesByte;
        switch (parity) {
            case PARITY_NONE: parityBitesByte = 0; break;
            case PARITY_ODD: parityBitesByte = 1; break;
            case PARITY_EVEN: parityBitesByte = 2; break;
            case PARITY_MARK: parityBitesByte = 3; break;
            case PARITY_SPACE: parityBitesByte = 4; break;
            default: throw new IllegalArgumentException("Bad value for parity: " + parity);
        }

        byte[] msg = {
                (byte) ( baudRate & 0xff),
                (byte) ((baudRate >> 8 ) & 0xff),
                (byte) ((baudRate >> 16) & 0xff),
                (byte) ((baudRate >> 24) & 0xff),
                stopBitsByte,
                parityBitesByte,
                (byte) dataBits};
        sendControlMessage(SET_LINE_CODING, 0, msg);
    }
    
    private int sendControlMessage(int request, int value, byte[] buf) {
        return mConnection.controlTransfer(
                USB_RT_ACM, request, value, 0, buf, buf != null ? buf.length : 0, 5000);
    }

    @Override
    public boolean getCD() throws IOException {
        return false;  // TODO
    }

    @Override
    public boolean getCTS() throws IOException {
        return false;  // TODO
    }

    @Override
    public boolean getDSR() throws IOException {
        return false;  // TODO
    }

    @Override
    public boolean getDTR() throws IOException {
        return mDtr;
    }

    @Override
    public void setDTR(boolean value) throws IOException {
        mDtr = value;
        setDtrRts();
    }

    @Override
    public boolean getRI() throws IOException {
        return false;  // TODO
    }

    @Override
    public boolean getRTS() throws IOException {
        return mRts;
    }

    @Override
    public void setRTS(boolean value) throws IOException {
        mRts = value;
        setDtrRts();
    }

    private void setDtrRts() {
        int value = (mRts ? 0x2 : 0) | (mDtr ? 0x1 : 0);
        sendControlMessage(SET_CONTROL_LINE_STATE, value, null);
    }
    
    public static Map<Integer, int[]> getSupportedDevices() {
        final Map<Integer, int[]> supportedDevices = new LinkedHashMap<Integer, int[]>();
        supportedDevices.put(Integer.valueOf(UsbId.VENDOR_TI),
                new int[] {
                    UsbId.TI_TUSB3410,
                });
        return supportedDevices;
    }
    
 

    
    public int readln(byte[] dest, byte EOL, int timeoutMillis) throws IOException {
        int counter = 0;
        final byte[] tmpRead = new byte[1];
        int readAmt = 1;
        synchronized (mReadBufferLock) {
            final UsbRequest request = new UsbRequest();
            request.initialize(mConnection, mReadEndpoint);

            final ByteBuffer buf = ByteBuffer.wrap(tmpRead);
            if (!request.queue(buf, readAmt)) {
                throw new IOException("Error queueing request.");
            }

            final UsbRequest response = mConnection.requestWait();
            if (response == null) {
                throw new IOException("Null response");
            }

            dest[0] = tmpRead[0];
            boolean EOLDetected = false;
            int result = 1;
            int numBytesRead;

            while (!EOLDetected)
            {

                readAmt = Math.min(dest.length-result, mReadBuffer.length);
                numBytesRead = mConnection.bulkTransfer(mReadEndpoint, mReadBuffer, readAmt,
                        timeoutMillis);
                if (numBytesRead > 0) {
                    for (int i = 0; i < numBytesRead; i++)
                    {
                        dest[result] = mReadBuffer[i];
                        result++;
                        if (mReadBuffer[i] == EOL)
                            EOLDetected = true;
                    }
                }

            }

            return result;
        }
    }

}
 
